package org.codefilarete.tool.collection;

import javax.annotation.Nullable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @author Guillaume Mary
 */
public class Arrays {
	
	private static final Integer[] EMPTY_INTEGER_ARRAY = {};
	private static final int[] EMPTY_INT_ARRAY = {};
	private static final long[] EMPTY_LONG_ARRAY = {};
	
	@SafeVarargs
	public static <T> List<T> asList(T... a) {
		return new ArrayList<>(java.util.Arrays.asList(a));
	}
	
	@SuppressWarnings("squid:S1319")	// LinkedHashSet return type is voluntary because it is the goal of this method
	@SafeVarargs
	public static <T> LinkedHashSet<T> asSet(T ... a) {
		LinkedHashSet<T> toReturn = new LinkedHashSet<>();
		java.util.Collections.addAll(toReturn, a);
		return toReturn;
	}
	
	@SuppressWarnings("squid:S1319")	// HashSet return type is voluntary because it is the goal of this method
	@SafeVarargs
	public static <T> HashSet<T> asHashSet(T ... a) {
		HashSet<T> toReturn = new HashSet<>();
		java.util.Collections.addAll(toReturn, a);
		return toReturn;
	}
	
	@SuppressWarnings("squid:S1319")	// TreeSet return type is voluntary because it is the goal of this method
	@SafeVarargs
	public static <T> TreeSet<T> asTreeSet(Comparator<? super T> comparator, T ... a) {
		TreeSet<T> toReturn = new TreeSet<>(comparator);
		java.util.Collections.addAll(toReturn, a);
		return toReturn;
	}
	
	@SuppressWarnings("squid:S1319")	// TreeSet return type is voluntary because it is the goal of this method
	public static <T> TreeSet<T> asTreeSet(Comparator<? super T> comparator, Collection<T> a) {
		TreeSet<T> toReturn = new TreeSet<>(comparator);
		toReturn.addAll(a);
		return toReturn;
	}
	
	public static boolean isEmpty(Object[] array) {
		return array == null || array.length == 0;
	}
	
	/**
	 * Tranforms an array of primitive integers to an array of Object integers to overcome the impossibility to cast a {@code int[]} to a {@code Integer[]}
	 * in the Java language.
	 * 
	 * @param integers any {@code int[]}, null included
	 * @return a new {@code Integer[]} that contains all values of passed argument
	 */
	@Nullable
	public static Integer[] fromPrimitive(@Nullable int[] integers) {
		if (integers == null) {
			return null;
		} else if (integers.length == 0) {
			return EMPTY_INTEGER_ARRAY;
		} else {
			Integer[] result = new Integer[integers.length];
			for (int i = 0; i < integers.length; i++) {
				result[i] = integers[i];
			}
			return result;
		}
	}
	
	/**
	 * Unboxes given {@link Integer}s
	 * 
	 * @param integers array to be transformed as a primitive one
	 * @return an array containing equivalent values of given array, null if input was null
	 */
	@Nullable
	public static int[] toPrimitive(@Nullable Integer[] integers) {
		if (integers == null) {
			return null;
		} else if (integers.length == 0) {
			return EMPTY_INT_ARRAY;
		} else {
			int[] result = new int[integers.length];
			for (int i = 0; i < integers.length; i++) {
				result[i] = integers[i];
			}
			return result;
		}
	}
	
	/**
	 * Unboxes given {@link Long}s
	 *
	 * @param longs array to be transformed as a primitive one
	 * @return an array containing equivalent values of given array, null if input was null
	 */
	@Nullable
	public static long[] toPrimitive(@Nullable Long[] longs) {
		if (longs == null) {
			return null;
		} else if (longs.length == 0) {
			return EMPTY_LONG_ARRAY;
		} else {
			long[] result = new long[longs.length];
			for (int i = 0; i < longs.length; i++) {
				result[i] = longs[i];
			}
			return result;
		}
	}
	
	/**
	 * Method that can be used as a method reference for array index access.
	 * 
	 * @param index index of the array to return
	 * @param <C> type of the array to use
	 * @return a function that can be used as a reference for array index acccess
	 */
	public static <C> Function<C[], C> get(int index) {
		return cs -> cs[index];
	}
	
	/**
	 * Method that can be used as a method reference for array index access. Will call the {@link Supplier} in case of index that is out of the array
	 * boundaries.
	 *
	 * @param index index of the array to return
	 * @param defaultValue the value to return when boundaires are reached (negative index or higher than array length)
	 * @param <C> type of the array to use
	 * @return a function that can be used as a reference for array index acccess
	 */
	public static <C> Function<C[], C> get(int index, Supplier<C> defaultValue) {
		return cs -> isOutOfBounds(index, cs) ? defaultValue.get() : cs[index];
	}
	
	private static <C> boolean isOutOfBounds(int index, C[] cs) {
		return index < 0 || index > cs.length-1;
	}
	
	public static <C> C first(C[] args) {
		return args[0];
	}
	
	public static <C> C last(C[] args) {
		return args[args.length-1];
	}
	
	/**
	 * Concats 2 arrays of int
	 * @param src1 an array
	 * @param src2 an array
	 * @return a new array of length src1 + src2 containing the aggregation of both
	 */
	public static int[] cat(int[] src1, int[] src2) {
		int[] result = new int[src1.length + src2.length];
		System.arraycopy(src1, 0, result, 0, src1.length);
		System.arraycopy(src2, 0, result, src1.length, src2.length);
		return result;
	}
	
	/**
	 * Concats 2 arrays of long
	 * @param src1 an array
	 * @param src2 an array
	 * @return a new array of length src1 + src2 containing the aggregation of both
	 */
	public static long[] cat(long[] src1, long[] src2) {
		long[] result = new long[src1.length + src2.length];
		System.arraycopy(src1, 0, result, 0, src1.length);
		System.arraycopy(src2, 0, result, src1.length, src2.length);
		return result;
	}
	
	/**
	 * Concats 2 arrays of objects
	 * 
	 * @param src1 an array
	 * @param src2 an array
	 * @return a new array of length src1 + src2 containing the aggregation of both
	 */
	public static <E> E[] cat(E[] src1, E[] src2) {
		return cat(src1, src2, newInstance(src1, src1.length + src2.length));
	}
	
	private static <E> E[] cat(E[] src1, E[] src2, E[] dest) {
		System.arraycopy(src1, 0, dest, 0, src1.length);
		System.arraycopy(src2, 0, dest, src1.length, src2.length);
		return dest;
	}
	
	/**
	 * Gives the headSize firsts elements of the given array
	 * 
	 * @param src a source array
	 * @param headSize number of elements to be kept in final result
	 * @param <E> array elements type
	 * @return the headSize firsts elements of the given array
	 */
	public static <E> E[] head(E[] src, int headSize) {
		E[] result = newInstance(src, headSize);
		System.arraycopy(src, 0, result, 0, headSize);
		return result;
	}
	
	/**
	 * Gives the headSize firsts elements of the given array
	 * 
	 * @param src a source array
	 * @param headSize number of elements to be kept in final result
	 * @return the headSize firsts elements of the given array
	 */
	public static long[] head(long[] src, int headSize) {
		return (long[]) arrayCopy(src, headSize, new long[headSize]);
	}
	
	/**
	 * Gives the headSize firsts elements of the given array
	 * 
	 * @param src a source array
	 * @param headSize number of elements to be kept in final result
	 * @return the headSize firsts elements of the given array
	 */
	public static int[] head(int[] src, int headSize) {
		return (int[]) arrayCopy(src, headSize, new int[headSize]);
	}
	
	/**
	 * Gives the headSize firsts elements of the given array
	 * 
	 * @param src a source array
	 * @param headSize number of elements to be kept in final result
	 * @return the headSize firsts elements of the given array
	 */
	public static byte[] head(byte[] src, int headSize) {
		return (byte[]) arrayCopy(src, headSize, new byte[headSize]);
	}
	
	private static Object arrayCopy(Object src, int headSize, Object result) {
		System.arraycopy(src, 0, result, 0, headSize);
		return result;
	}
	
	/**
	 * Gives the tailSize lasts elements of the given array
	 *
	 * @param src a source array
	 * @param tailSize number of elements to be kept in final result
	 * @param <E> array elements type
	 * @return the headSize lasts elements of the given array
	 */
	public static <E> E[] tail(E[] src, int tailSize) {
		E[] result = newInstance(src, tailSize);
		System.arraycopy(src, src.length - tailSize, result, 0, tailSize);
		return result;
	}
	
	private static <E> E[] newInstance(E[] template, int length) {
		return (E[]) Array.newInstance(template.getClass().getComponentType(), length);
	}
	
	private Arrays() {
		// tool class
	}
}
